/**
 * the implements of ssISiteManager
 *
 * related events:
 *  'site-added'      - (evt, [index1, index2, ...])
 *  'site-updated'    - (evt, index)
 *  'site-removed'    - (evt, [index1, index2, ...])
 *  'sites-reloaded'  - (evt, null)
 */
var EXPORTED_SYMBOLS = [ "ssSiteManager" ];

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Ce = Components.Exception;
const Cu = Components.utils;
const nsISupports = Ci.nsISupports;
const ssISiteManager = Ci.ssISiteManager;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");

function ssSiteManager() {
	let that = this;

	let logger = this.logger;

	let snapshotLoading = 'images/loading.gif';
	let snapshotNoImage = 'images/no-image.png';
	let favIcon = 'chrome://mozapps/skin/places/defaultFavicon.png';

	let sites2bFetched = [];
	let fetchingCount = 0;
	let fetchingMax = 5;
	let snapshotBrowsers = {};

	let sitesPerLine = this.getConfig('site-perline');
	let minimumLines = this.getConfig('site-minimum-lines');
	let imgWidth = 212, imgHeight = 132;
	function getEmptySite() {
		return {
			url: null, title: null, name: null, snapshot: null
		};
	}

	let sitesFile = FileUtils.getFile('ProfD', ['superstart', 'sites.json']);
	load();


	////////////////////////////////////////////////////////////////////////////////
	// event subscription
	let evts = {
		'site-perline' : onSitePerLineChanged,
		'site-minimum-lines' : onSiteMinimumLinesChanged
	};
	for (let k in evts) {
		this.subscribe(k, evts[k]);
	}
	function onSitePerLineChanged() {
		that.sitesReload();
	}
	function onSiteMinimumLinesChanged() {
		that.sitesReload();
	}


	////////////////////////////////////////////////////////////////////////////////
	// add site
	this.addSite = function(url, name, image) {
		let sites = this.sites;
		for (var i = 0, l = sites.length; i < l; ++ i) {
			if (sites[i].url == null) {
				break;
			}
		}

		let indexes = [];
		if (i == l) {
			// add an empty site
			sites.push(getEmptySite());
			indexes.push(i);

			// add some empty sites to fill a line
			let count = sites.length;
			if (count % sitesPerLine != 0) {
				count = sitesPerLine - (count % sitesPerLine);
				for (let j = 0; j < count; ++ j) {
					sites.push(getEmptySite());
					indexes.push(i + j + 1);
				}
			}

			save();

			this.fireEvent('site-added', indexes);
		}

		this.changeSite(i, url, name, image);

	}

	this.changeSite = function(index, url, name, image) {
		let sites = this.sites;
		if (index < 0 || index >= sites.length) {
			return;
		}

		url = this.regulateUrl(url);

		let site = sites[index];
		if (url == site.url) {
			// if name is changed...
			if (name != site.name || image != site.image) {
				site.name = name;
				if (image == '') {
					delete site.image;
				} else {
					site.image = image;
				}
				save();
				this.fireEvent('site-updated', [index]);
			}
			return;
		}

		// we first remove the original site
		this.removeSite(index);

		if (url == null) {
			return;
		}

		site = sites[index]; // protect for this.sites[index] is changed by 'removeSite'

		// then set the new value
		let snapshot = snapshotNoImage;
		let icon = favIcon;
		let title = url;

		site.url = url;
		site.title = title;
		site.name = name;
		site.icon = icon;
		site.snapshot = snapshot;
		if (image != '') {
			site.image = image;
		}

		// the 'site-updated' event will be fired in refreshSite(), so we don't fire it here

		this.refreshSite(index);
	}

	this.exchangeSite = function (index1, index2) {
		if (index1 < 0 || index1 >= this.sites.length || index2 < 0 || index2 >= this.sites.length) {
			return;
		}

		let tmp = this.sites[index1];
		this.sites[index1] = this.sites[index2];
		this.sites[index2] = tmp;

		save();

		this.fireEvent('site-updated', [index1, index2]);

		alignSitesLine();
	}


	this.removeSite = function(index) {
		let sites = this.sites;
		if (index < 0 || index >= sites.length) {
			return;
		}

		let site = sites[index];
		let url = site.url;
		if (url != null ) {
			clearSnapshot(index);
			sites[index] = getEmptySite();
			save();

			this.fireEvent('site-updated', [index]);
	
			alignSitesLine();
		}
	}


	this.refreshSite = function(index) {
		let sites = this.sites;
		if (index == -1) {
			for (let i = 0, l = sites.length; i < l; ++ i) {
				this.refreshSite(i);
			}
			return;
		}

		if (index < 0 || index >= sites.length) {
			return;
		}

		let site = sites[index];
		let url = site.url;
		if (url == null) {
			return; // this shouldn't happen, unless it is called by this.refreshSite(-1)
		}

		// refresh all the same url, so just delete snapshot file, it is safe!
		// find all the sites with the same url
		let indexes = [];
		let snapshotRemoved = false;
		for (let i = 0, l = sites.length; i < l; ++ i) {
			let s = sites[i];
			if (s.url == url) {
				let sn = s.snapshot;
				if (!snapshotRemoved && sn && sn.indexOf('images/') != 0) {
					removeSnapshotFile(s.snapshot);
					snapshotRemoved = true;
				}
				s.snapshot = snapshotLoading;
				indexes.push(i);
			}
		}

		save();

		this.fireEvent('site-updated', indexes);

		fetchSite(site.url);
	}

	this.sitesReload = function() {
		sitesPerLine = this.getConfig('site-perline');
		minimumLines = this.getConfig('site-minimum-lines');
		load();
		this.fireEvent('sites-reloaded', null);
	}


	////////////////////////////////////////////////////////////////////////////////
	// get sites
	this.getSites = function() {
		// make a clone
		let s = this.jsonEncode(this.sites);
		let sites = this.jsonDecode(s);

		for (let i = 0, l = sites.length; i < l; ++ i) {
			preprocessSite(sites[i]);
		}
		return sites;
	}

	this.getSite = function(index) {
		let site = getEmptySite();
		if (index >= 0 && index < this.sites.length) {
			site = this.sites[index];
		}
		site = this.jsonDecode(this.jsonEncode(site));
		preprocessSite(site);
		return site;
	}

	////////////////////////////////////////////////////////////////////////////////
	// load & save

	function alignSitesLine() {
		let minCount = sitesPerLine * minimumLines;
		let count = that.sites.length;
		let indexes = [];

		// 1. too little, if so, 2) and 3) are not needed
		if (count < minCount) {
			for (let i = count; i < minCount; ++ i) {
				that.sites.push(getEmptySite());
				indexes.push(i);
			}
			save();
			that.fireEvent('site-added', indexes);
			return;
		}

		// 2. not align
		if (count % sitesPerLine != 0) {
			let addCount = sitesPerLine - (count % sitesPerLine);
			for (let i = 0; i < addCount; ++ i) {
				that.sites.push(getEmptySite());
				indexes.push(count ++);
			}
			save();
			that.fireEvent('site-added', indexes);
			indexes = [];
		}

		// 3. empty lines ?
		if (count != that.sites.length) {
			count = that.sites.length;
		}
		if (count % sitesPerLine != 0) {
			count = that.sites.length;
		}
		let rows = count / sitesPerLine;
		if (rows > minimumLines) {
			indexes = [];
			for (let l = rows - 1; l >= minimumLines; -- l) {
				let idxes = [];
				for (let i = l * sitesPerLine; i < (l + 1) * sitesPerLine; ++ i) {
					if (that.sites[i].url != null) {
						break;
					} else {
						idxes.push(i);
					}
				}

				if (idxes.length == sitesPerLine) {
					for (let i = 0; i < sitesPerLine; ++ i) {
						indexes.push(idxes[i]);
					}
				}
			}

			if (indexes.length > 0) {
				for (let i = 0, l = indexes.length; i < l; ++ i) {
					that.sites.pop();
				}
				save();
				that.fireEvent('site-removed', indexes);
			}
		}
	}

	// load sites from 'superstart/sites.json'
	function load() {
		that.sites = [];
		for (let i = 0; i < sitesPerLine * minimumLines; ++ i) {
			that.sites.push(getEmptySite());
		}
		let retry = 0;
		do {
			try {
				if (!sitesFile.exists() || retry > 0) {
					if (retry == 0) {
						sitesFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
					}
					// reset the content to empty ...
					save();
				}
				let sites = that.jsonDecode(that.fileGetContents(sitesFile));
				if (sites.length == 0) {
					save();
				} else {
					that.sites = sites;
				}
				sites = null;
				alignSitesLine();
				break;
			} catch (e) {
				logger.logStringMessage(e);
				retry ++;
			}
		} while (retry < 2);

		// is there are any 'loading' ?
		let changed = false;
		for (let i = 0, l = that.sites.length; i < l; ++ i) {
			let site = that.sites[i];
			if (site.snapshot == 'images/loading.gif') {
				site.snapshot = 'images/no-image.png';
				changed = true;
			}
		}

		// is there any incorrect url
		for (let i = 0, l = that.sites.length; i < l; ++ i) {
			let site = that.sites[i];
			let urlRegulated = that.regulateUrl(site.url);
			if (urlRegulated != site.url) {
				site.url = urlRegulated;
				changed = true;
			}
		}

		if (changed) {
			save();
		}
	}

	function save() {
		that.filePutContents(sitesFile, that.jsonEncode(that.sites));
	}

	////////////////////////////////////////////////////////////////////////////////
	// snapshot related
	//

	function getImageFileFromFileName(imageFile) {
		return FileUtils.getFile('ProfD', ['superstart', 'snapshots', imageFile]);
	}

	function getImagePathFromFileName(imageFile) {
		return getImageFileFromFileName(imageFile).path;
	}

	function preprocessSite(site) {
		if (site.name && site.name != '') {
			site.displayName = site.name;
		} else {
			site.displayName = site.title;
		}

		processSnapshot(site);
	}

	function processSnapshot(site) {
		if (site.snapshot && site.snapshot.indexOf('images/') != 0) {
			let snapshot = getImagePathFromFileName(site.snapshot);
			snapshot = snapshot.replace(/\\/g, '/');
			site.snapshot = that.regulateUrl(snapshot);
		}
	}

	function removeSnapshotFile(snapshot) {
		try {
			let path = '';
			if (snapshot && snapshot.indexOf('images/') != 0) {
				let sn = getImageFileFromFileName(snapshot);
				path = sn.path;
				sn.remove(false);
			}
		} catch (e) {
			logger.logStringMessage('remove file: ' + path + ' failed, exception is below:');
			logger.logStringMessage(e);
		}
	}

	/**
	 * the caller should call 'save()' itself!!
	 */
	function clearSnapshot(index) {
		let sites = that.sites;
		if (index < 0 || index >= sites.length) {
			return;
		}

		let site = sites[index];
		let sn = site.snapshot;
		if (sn != null && sn.indexOf('images/') == -1) {
			// search for the same snapshot
			let found = false;
			for (let i = 0, l = sites.length; i < l; ++ i) {
				if (i != index && sites[i].snapshot == sn) {
					found = true;
					break;
				}
			}
			if (!found) {
				removeSnapshotFile(site.snapshot);
			}
		}
	}


	function getIcon(doc) {
		try {
			let loc = doc.location;
			if (loc.href.indexOf('http') == 0) {
				let links = doc.getElementsByTagName('link');
				// 1. look for rel="shortcut icon"
				for (let i = 0, l = links.length; i < l; ++ i) {
					let link = links[i];
					let rel = link.rel || '';
					if (rel.search(/icon/i) != -1 && rel.search(/shortcut/i) != -1) {
						return link.href;
					}
				}

				// 2. icon only
				for (let i = 0, l = links.length; i < l; ++ i) {
					let link = links[i];
					let rel = link.rel || '';
					if (rel.search(/icon/i) != -1) {
						return link.href;
					}
				}

				// 3. 
				if (loc.protocol == 'http:' || loc.protocol == 'https:') {
					return loc.protocol + '//' + loc.host + '/favicon.ico';
				}
			}
		} catch (e) {
			logger.logStringMessage(e);
		}
		return favIcon;
	}

	function setSiteInformation(index, title, icon, snapshot, image) {
		if (index < 0 || index >= that.sites.length) {
			logger.logStringMessage('index out of range: ' + index + '[0, ' + that.sites.length + ']!');
			return;
		}

		let sites = that.sites;
		let site = sites[index];
		site.title = title;
		site.icon = icon;

		clearSnapshot(index);
		site.snapshot = snapshot;
		if (image != undefined) {
			site.image = image;
		} else {
			delete site['image'];
		}
		save();

		that.fireEvent('site-updated', [index]);
	}

	function fetchSite(url) {
		for (let i = 0, l = sites2bFetched.length; i < l; ++ i) {
			if (url == sites2bFetched[i]) {
				return;
			}
		}

		sites2bFetched.push(url);

		doFetchSite();
	}

	function doFetchSite() {
		if (fetchingCount >= fetchingMax || sites2bFetched.length == 0) {
			return;
		}

		++ fetchingCount;
		let url = sites2bFetched.shift();
		if (snapshotBrowsers[url]) { // if it is fetching...
			-- fetchingCount;
			return;
		}

		function getImageFileNameFromUrl(url) {
			return SHA1(url + Math.random()) + '.png';
			// return SHA1(url) + '.png';
		}

		// get the window
		let wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
		let gWindow = wm.getMostRecentWindow("navigator:browser");

		let width = imgWidth, height = imgHeight;
		let fw = 1024, fh = Math.floor(fw * height / width);

		let gDoc = gWindow.document;
		let container = gDoc.getElementById('superstart-snapshot-container');
		let browser = gDoc.createElement('browser');
		snapshotBrowsers[url] = browser; // save it
		// set the browser attributes
		browser.width = fw;
		browser.height = fh;
		browser.setAttribute('type', 'content');
		// browser.setAttribute("disablehistory", "true"); // google+ need history, otherwise it will display a blank page. it sucks...(maybe g+ should use a try...catch... for its ajax page nagivation...)
		browser.setAttribute('src', url);

		container.appendChild(browser);

		let now = (new Date()).getTime();
		let timeout = 30 * 1000;
		let timeoutId = gWindow.setTimeout(onTimeout, timeout);
		browser.addEventListener('load', onLoad, true);

		function onLoad() {
			gWindow.clearTimeout(timeoutId);

			timeout = ((new Date()).getTime() - now) * 3;
			if (timeout < 1000) {
				timeout = 1000; // wait at least 1s
			} else if (timeout > 30 * 1000) {
				timeout = 30 * 1000; // but not exceed 30s
			}

			timeoutId = gWindow.setTimeout(onTimeout, timeout);
		}

		function onTimeout() {
			browser.removeEventListener('load', onLoad, true);

			let doc = browser.contentDocument;
			let title = doc.title || url;
			let icon = getIcon(doc);
			doc = null;

			let snapshot = getImageFileNameFromUrl(url);
			let pathName = getImagePathFromFileName(snapshot);
			let canvas = window2canvas(gDoc, browser, width, height);
			saveCanvas(canvas, pathName, function() {
				let sites = that.sites;
				let isSnapshotUsed = false;
				for (let i = 0, l = sites.length; i < l; ++ i) {
					if (sites[i].url == url) {
						setSiteInformation(i, title, icon, snapshot, sites[i].image);
						isSnapshotUsed = true;
					}
				}

				if (!isSnapshotUsed) {
					removeSnapshotFile(snapshot);
				}

				afterFetched();
			});
		}

		function afterFetched() {
			container.removeChild(browser);
			delete snapshotBrowsers[url];
			browser = null;

			if (fetchingCount > 0) {
				-- fetchingCount;
			} else {
				logger.logStringMessage('*** fetchingCount incorrect! ***');
			}
			
			if (sites2bFetched.length > 0) {
				doFetchSite();
			}
		}
	}

	function window2canvas(doc, win, width, height) {
		try {
			doc = win.contentDocument;
			let html = doc.getElementsByTagName('html');
			if (html.length > 0) {
				html = html[0];
				var ww = html.clientWidth;
				if (ww > win.clientWidth) {
					ww = win.clientWidth;
				}
			} else {
				var ww = win.clientWidth;
			}
			var wh = win.clientHeight;
		} catch (e) {
			var ww = win.clientWidth, wh = win.clientHeight;
		}
		let ow = ww, oh = wh;

		let contentWindow = win.contentWindow;
		let canvas = doc.createElementNS("http://www.w3.org/1999/xhtml", "html:canvas");

		canvas.style.width = width + 'px';
		canvas.style.height = height + 'px';
		canvas.width = width;
		canvas.height = height;

		let ctx = canvas.getContext('2d');
		ctx.clearRect(0, 0, width, height);
		ctx.save();
		ctx.mozImageSmoothingEnabled = true;
		ctx.scale(width / ww, height / wh);
		let x = 0, y = 0;
		if (ow > ww) {
			x = (ow - ww) / 2;
		}
		if (oh > wh) {
			y = (oh - wh) / 2;
		}
		ctx.drawWindow(contentWindow, x, y, ww, wh, "rgb(255, 255, 255)");
		ctx.restore();
		return canvas;
	}

	function saveCanvas(canvas, pathName, callback) {
		let file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsILocalFile);
		file.initWithPath(pathName);
		let io = Cc['@mozilla.org/network/io-service;1'].getService(Ci.nsIIOService);
		let src = io.newURI(canvas.toDataURL('image/png', ''), 'UTF8', null);
		let persist = Cc['@mozilla.org/embedding/browser/nsWebBrowserPersist;1'].createInstance(Ci.nsIWebBrowserPersist);
		persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
		persist.persistFlags |= Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;

		let listener = {
			onStateChange: function(webProgress, req, flags, aStatus) {
						if (flags & Ci.nsIWebProgressListener.STATE_STOP) {
							persist.progressListener = null;
							persist = null;
							if (callback) {
						 		callback();
								callback = null;
							}
						}
					}
		}
		persist.progressListener = listener;
		try {
			persist.saveURI(src, null, null, null, null, file);
		} catch (e) {
			persist.saveURI(src, null, null, null, null, file, null);
		}
	}

	function SHA1 (msg) {
		function tohex(s) {
			let hc = '0123456789ABCDEF';
			let he = new Array(s.length * 2);
			for (let i = 0; i < s.length; ++ i) {
				let c = s.charCodeAt(i);
				he[i * 2] = hc.charAt((c >> 4) & 15);
				he[i * 2 + 1] = hc.charAt(c & 15);
			}
			return he.join('');
		}

		let h = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
		h.init(Ci.nsICryptoHash.SHA1);
		let s = Cc['@mozilla.org/io/string-input-stream;1'].createInstance(Ci.nsIStringInputStream);
		s.setData(msg, msg.length);
		h.updateFromStream(s, s.available());
		let m = h.finish(false);
		s = h = null;

		return tohex(m);
	}
}
